<?php
/**
 *  The properties are stored in the __dict__:
 *  - to get a model's property, simple offsetGet() is OK
 *  - to iterate over properties, iterate over the model
 */
abstract class Yz_Model_Abstract
    extends Yz_Class
    implements Yz_Model_Interface
{

    /** The loaded models (avoid adding $instance to each model class)
     *  @var array (className: model)
     */
    private static $instances = array();

    /**
     *  @var string
     */
    protected static $propertyConfig = '';

    /** Reference to the module
     *  @var Yz_Module_Interface
     */
    protected $module;

    /**
     *  @var Yz_Config_Interface
     */
    protected $config;

    /** The class name of model's items
     *  @var string
     */
    protected $itemClass;

    /** The list of model's items
     *  @var Yz_List
     */
    protected $items;

    /** The name of the model
     *  @var string
     */
    protected $name;

    /** The class' reflection object
     *  @var string|null
     */
    protected $reflection;

    /** The name of the source used by the model
     *  @var string|null
     */
    protected $sourceName;

    /** The parent model, if any
     *  @var Yz_Model_Interface|null
     */
    protected $parentModel;


    /****************************
     *      Public methods      *
     ****************************/


    /** Model's constructor requires module
     *  @param Yz_Module_Interface $module
     */
    public static function getInstance( Yz_Module_Interface $module )
    {
        debug();
        $myclass = get_called_class();
        $i =& self::$instances;
        if (isset($i[$myclass]))
            $obj = $i[$myclass];
        else
            $i[$myclass] = $obj = new static($module);
        return $obj;
    }


    /** Model's constructor requires module
     *  @param Yz_Module_Interface $module
     */
    protected function __construct( Yz_Module_Interface $module )
    {
        debug();
        $this->module = $module;
        $this->name = $this->shortClassName();
        $this->init();
    }

    /** Model's string representation
     *  @return string
     */
    public function __toString()
    {
        $moduleName = $this->module->getFullName();
        return "Model({$moduleName}.{$this->name})";
    }

    /** get Model's name
     *  @return string
     */
    final public function getName()
    {
        return $this->name;
    }

    /** get Model's module
     *  @return string
     */
    final public function getModule()
    {
        return $this->module;
    }

    /** get Model's config
     *  @return Yz_Config_Interface
     */
    public function getConfig()
    {
        debug();
        $config =& $this->config;
        if (null === $config) {
            //
            throw new Yz_Exception_NotImplemented;
        }
        return $config;
    }

    /** get Model's parent model
     *  @return string
     */
    final public function getParentModel()
    {
        debug();
        return $this->parentModel;
    }

    /** get Model's source name
     *  @return string
     */
    public function getSourceName()
    {
        debug();
        $sName =& $this->sourceName;
        if (! $sName) {
            $sName = strtolower($this->module->getFullName());
            $me = $this->myReflection();
            if ($me->hasConstant('SOURCE_NAME'))
                $sName = str_replace('~', $sName, $me->getConstant('SOURCE_NAME'));
        }
        return $sName;
    }

    /**
     *  @return Yz_Model_Interface
     */
    public function selectModel($modelName)
    {
        debug();
        if ($modelName === $this->name) {
            $model = $this;
        } else {
            $model = $this->module->selectModel($modelName);
        }

        return $model;
    }


    /** get the associated item class
     *  @return string class(Yz_Item_Interface)
     */
    public function getItemClass()
    {
        debug();
        $class =& $this->itemClass;
        if (! $class)
            $class = $this->module->resolveClass('Item_' . $this->name);
        return $class;
    }


    /** create new item object
     *  @return Yz_Item_Interface
     */
    public function newItem()
    {
        debug();
        $class = $this->getItemClass();
        return new $class($this);
    }


    /** create new item object from values
     *  @return Yz_Item_Interface
     */
    public function itemFromValues($values, $prefix=null)
    {
        debug("Prefix={$prefix}");
        $myprefix = $prefix ?: $this->name . '_';
        $id = $myprefix . 'id';
        $id = $values[$id];
        if (! $id)
            throw Yz_Item_Exception::_new('Bad values');
        if ($this->items->offsetExists($id)) {
            // assume that any referenced items have also been assigned
            $item = $this->items[$id];
        } else {
            $class = $this->getItemClass();
            $item = new $class($this, $values, $prefix);
            $this->items[$id] = $item;

            // NOTE: if the parent model's item with the ID belongs to this model,
            // it NECESSARILY exists in this model's list
            // (in case the item was fetched via parent model, its type will be that of parent)
            // TODO instead of directly assigning item to parent's list, if item is of parent type,
            // add a reference to this item, so that consistency is not broken
            // (when modifying parent, this item must also get modified!)
            if ($this->parentModel)
                $this->parentModel->items[$id] = $item;
        }
        return $item;
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param varargs args
     *  @return Yz_Item_Interface
     */
    public function fetchItem(/* $args */)
    {
        debug();
        return $item;
    }

    /** fetch item from the storage source
     *      Parameters:
     *  @param id $id
     *  @return Yz_Item_Interface
     */
    public function fetchItemById($id)
    {
        debug();
        if ($this->items->offsetExists($id))
            return $this->items->offsetGet($id);
        $action = $this->selectAction('SelectById');
        $values = $action
            ->setId($id)
            ->perform()
            ->fetch()
        ;
        return $this->itemFromValues($values);
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param varargs args
     *  @return Yz_Array{Yz_Item_Interface}
     */
    public function fetchList()
    {
        debug();
        $action = $this->selectAction('SelectList');
        $values = $action
            ->perform()
            ->fetch()
        ;
        return $this->assignList($values);
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param Yz_Item_Interface|Yz_List args
     *  @return Yz_Array{Yz_Item_Interface}
     */
    public function fetchListByKey($keyName, $args)
    {
        debug();
        // 1. identify which foreign key references the item
        if (! isset($this->__dict__[$keyName]))
            throw Yz_Model_Exception::_new('No such property');
        $keyName .= 'Id';
        // 2. identify the key value[s]
        if ($args instanceof Yz_Item_Interface) {
            $keys = $args->id;
        } else if ($args instanceof Yz_List) {
            $keys = array_keys($keys->toArray());
        } else {
            throw Yz_Model_Exception::_new('Invalid argument');
        }
        $action = $this->selectAction('SelectListByKey');
        $values = $action
            ->setKey($keyName, $keys)
            ->perform()
            ->fetch()
        ;
        return $this->assignList($values);
    }

    /**
     *  fetch hash of items indexed by id, using pagination (p8n)
     *  @param Yz_Pagination_Interface
     *  @return Yz_Hash[Yz_Item_Interface]
     *  @throw NotFound
     */
    public function fetchPage(Yz_Pagination_Interface $p8n)
    {
        debug();
        $action = $this->selectAction('SelectList');
        $values = $action
            ->setOffset($p8n->getPageOffset())
            ->setLimit($p8n->getPageItems())
            ->perform()
            ->fetch()
        ;
        $p8n->setItemCount($action->getFoundRows());
        return $this->assignList($values);
    }

    /** fetch item's neighbors as array (0: previous, 1: next)
     *  @param Yz_Item_Interface
     *  @return array(Yz_Item_Interface)
     */
    public function fetchNeighbors(Yz_Item_Interface $item)
    {
        debug();
        if (! $item->isItemOf($this))
            throw Yz_Model_Exception::_new("Wrong item type");
        $action = $this->selectAction('SelectNeighbors');
        $values = $action
            ->setId($item->id)
            ->perform()
            ->fetch()
        ;
        $neighbors = array(null, null);
        if ($values) {
            foreach ($values as $v) {
                $itemN = $this->itemFromValues($v);
                $i = $itemN->id < $item->id ? 0 : 1;
                $neighbors[$i] = $itemN;
            }
        }
        return $neighbors;
    }


    /** update item in the storage source, using item id
     *  @param Yz_Item_Interface
     *  @return Yz_Item_Interface
     */
    public function updateItem(Yz_Item_Interface $item)
    {
        debug();
        if (! $item->isItemOf($this))
            throw Yz_Model_Exception::_new("Wrong item type");
        $action = $this->selectAction('UpdateItem');
        $item = $action
            ->setItem($item)
            ->perform()
        ;
        return $this;
    }

    /** Delete item from the storage source
     *  @todo check constraint for number of items
     *  @param Yz_Item_Interface
     *  @return this
     */
    public function deleteItem(Yz_Item_Interface $item)
    {
        debug();
        if (! $item->isItemOf($this))
            throw Yz_Model_Exception::_new("Wrong item type");
        $action = $this->selectAction('DeleteItem');
        $action
            ->setItem($item)
            ->perform()
        ;
        return $this;
    }

    /** Add item to the storage source
     *  @todo check constraint for number of items
     *  @param Yz_Item_Interface
     *  @return Yz_Item_Interface
     */
    public function addItem(Yz_Item_Interface $item)
    {
        debug();
        if (! $item->isItemOf($this))
            throw Yz_Model_Exception::_new("Wrong item type");
        $action = $this->selectAction('InsertItem');
        $id = $action
            ->setItem($item)
            ->perform()
            ->getInsertId()
        ;
        $item->id = $id;
        return $item;
    }

    /********************************
     *      Protected methods       *
     ********************************/

    /**
     *  identify and create action
     *  @param string $actionName
     *  @return Yz_Action_Interface
     */
    protected function selectAction( $actionName )
    {
        debug();
        $class = $this->module->resolveLibrary('Action', $actionName);
        assert('is_subclass_of($class,"Yz_Action_Interface")');

        // TODO take class name from property of $this
        return new $class($this);
    }


    /**
     *
     */
    protected function myReflection()
    {
        $r =& $this->reflection;
        $r or $r = new ReflectionClass($this);
        return $r;
    }

    /** Assign values from the database to an item
     *  @param Yz_Hash values { "ModelName_PropName" : mixed value }
     *  @return Yz_List(Yz_Item_Interface)
     */
    public function assignList($values)
    {
        $list = new Yz_List($this->getItemClass());
        foreach ($values as $row) {
            $item = $this->itemFromValues($row);
            $list[$item->id] = $item;
        }
        return $list;
    }




    /** the default setter for properties which don't have theirs own
     *  @param string index
     *  @return NEVER
     *  @throw Yz_Exception_ObjectModificationForbidden
     */
    protected function defaultSetter( $index, $value )
    {
        throw Yz_Exception_ObjectModificationForbidden::_new();
    }

    /** the default unsetter for properties which don't have theirs own
     *  @param string index
     *  @return NEVER
     *  @throw Yz_Exception_ObjectModificationForbidden
     */
    protected function defaultUnsetter( $index )
    {
        throw Yz_Exception_ObjectModificationForbidden::_new();
    }


    protected function init()
    {
        $this->itemClass = $this->module->resolveClass('Item_' . $this->name);
        $this->items = new Yz_List($this->itemClass);
        // subclasses put their property init here
        return $this;
    }


    protected function extend()
    {
        $parentClass = get_parent_class($this);
        debug("Extending $parentClass");
        $parentModel = $parentClass::getInstance($this->module);
        //$parentModel->__dict__['_child' . $this->name]
        $this->__dict__['_'.$parentModel->name] = new Yz_Property_OneToOne($parentModel);
        $this->parentModel = $parentModel;
        #parent::init();
//        $this->__dict__ += $dict;
    }

}
